
export type ICallback<T> = (self: LoopRetry<T>) => void
export type IResolveParam<T> = T | PromiseLike<T> | Error
export type IResolve<T> = (value: IResolveParam<T>) => void

export class LoopRetry<T extends any = any> {

    private readonly timeoutErrorMessage: string = 'The task timed out'
    private readonly maxRetryCountErrorMessage: string = 'The maximum number of retries has been reached'

    private retryCount: number = 0

    private retryDelay: number = 0

    private maxTime: number = 0

    private isRunning: boolean = false

    private resolve: IResolve<T> | null = null

    private result: IResolveParam<T> | null = null

    count: number = 0

    constructor(
        private callback: ICallback<T>,
        options: { retryCount?: number, retryDelay?: number, maxTime?: number } = {}
    ) {
        if (options?.retryCount !== undefined) this.retryCount = options.retryCount
        if (options?.retryDelay !== undefined) this.retryDelay = options.retryDelay
        if (options?.maxTime !== undefined) this.maxTime = options.maxTime
    }


    async start(): Promise<T> {

        if (this.isRunning) throw new Error('The task is currently running')

        this.isRunning = true

        return await new Promise<T>(async (resolve) => {

            this.resolve = resolve as IResolve<T>

            await this.doWork()

            this.resolve && this.resolve(this.result as IResolveParam<T>)

            this.reset()
        })
    }

    private reset() {
        this.isRunning = false
        this.count = 0
        this.resolve = null
        this.result = null
    }

    private async doWork() {

        const startTime = Date.now()

        while (true) {

            if (this.maxTime > 0 && Date.now() - startTime > this.maxTime) {
                this.result = new Error(this.timeoutErrorMessage)
                break
            }

            if (this.retryCount > 0 && this.count >= this.retryCount) {
                this.result = new Error(this.maxRetryCountErrorMessage)
                break
            }

            this.count++

            await this.callback(this)

            if (this.resolve === null) return

            await new Promise(resolve => setTimeout(resolve, this.retryDelay))

        }

    }


    complete(data: IResolveParam<T>) {
        this.resolve && this.resolve(data)
        this.resolve = null
    }

}